using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.IdGenerators;
using MongoDB.Bson.Serialization.Options;
using MongoScienceDAL.Core.Exceptions;

namespace MongoScienceDAL.Core
{
    public class ScienceEntrySerializer : IBsonSerializer, IBsonIdProvider
    {
        private readonly IDataSource _dataSource;

        public ScienceEntrySerializer(IDataSource dataSource)
        {
            _dataSource = dataSource;
        }

        public object Deserialize(BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
        {
            //check this is the correct type
            if (nominalType != typeof(ScienceEntry)) throw new ArgumentException("Cannot deserialize anything but ScienceEntry");

            var scienceEntry = new ScienceEntry();
            var document = BsonDocument.ReadFrom(bsonReader);

            //find collection
            var collectionName = document["_col"].AsString;
            var collections = _dataSource.QueryCollections().Where(x => x.Name == collectionName);
            if (collections.Any())
            {
                scienceEntry.Collection = collections.First();
            }
            else
            {
                throw new NoSuchCollectionException();
            }

            //Get science types for collection
            var types = _dataSource.QueryCollectionTypes(collectionName).ToList();

            //init data
            scienceEntry.Data = new Dictionary<string, object>();
            
            foreach (var name in document.Names)
            {
                //picks up the id
                if (name == "_id")
                {
                    scienceEntry.Id = document[name].AsObjectId;
                    continue;
                }

                //skip the collection name
                if (name == "_col")
                    continue;

                //if unknown name then skip
                if (types.All(x => x.Key != name))
                    continue;

                //now we get the type to convert to
                var scienceType = types.First(x => x.Key == name);

                //make the conversion
                switch (scienceType.Type)
                {
                    case ScienceTypeEnum.Text:
                        scienceEntry.Data.Add(name, document[name].AsString);
                        break;
                    case ScienceTypeEnum.DateTime:
                        scienceEntry.Data.Add(name, document[name].AsDateTime.ToLocalTime());
                        break;
                    case ScienceTypeEnum.Value:
                        scienceEntry.Data.Add(name, document[name].AsDouble);
                        break;
                    case ScienceTypeEnum.ValueSet:
                        var values = document[name].AsBsonArray;
                        var casted = BsonSerializer.Deserialize<List<double>>(values.ToJson());
                        scienceEntry.Data.Add(name, casted);
                        break;
                    case ScienceTypeEnum.Files:
                        scienceEntry.Data.Add(name, document[name]
                            .AsBsonArray
                            .Select(fileData => BsonSerializer.Deserialize<ScienceFile>(fileData.AsBsonDocument))
                            .ToList());
                        break;
                    case ScienceTypeEnum.VersionedFile:
                        scienceEntry.Data.Add(name, document[name]
                            .AsBsonArray
                            .Select(fileData => BsonSerializer.Deserialize<VersionedFile>(fileData.AsBsonDocument))
                            .ToList());
                        break;
                    case ScienceTypeEnum.ParentLink:
                        scienceEntry.Data.Add(name, document[name].AsObjectId);
                        break;
                    case ScienceTypeEnum.ChildLinks:
                        var childLinks = document[name].AsBsonArray;
                        var castedChildLinks = BsonSerializer.Deserialize<List<ObjectId>>(childLinks.ToJson());
                        scienceEntry.Data.Add(name, castedChildLinks);
                        break;
                    case ScienceTypeEnum.Boolean:
                        scienceEntry.Data.Add(name, document[name].AsBoolean);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            return scienceEntry;
        }

        public object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
        {
            return Deserialize(bsonReader, nominalType, options);
        }

        public IBsonSerializationOptions GetDefaultSerializationOptions()
        {
            return null;
        }

        public void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
        {
            //check this is the correct type
            if (nominalType != typeof(ScienceEntry)) throw new ArgumentException("Cannot serialize anything but ScienceEntry");

            //cast
            var scienceEntry = (ScienceEntry)value;

            //find collection
            var collection = scienceEntry.Collection;

            //header
            bsonWriter.WriteStartDocument();

            //create collection entry
            bsonWriter.WriteString("_col", collection.Name);

            //Get science types for collection
            var types = _dataSource.QueryCollectionTypes(collection.Name).ToList();

            //create writer helper
            var scienceEntryWriter = new ScienceEntryBsonWriter(bsonWriter);
            
            foreach (var field in scienceEntry.Data)
            {
                //if unknown name then skip
                if (types.All(x => x.Key != field.Key))
                    continue;

                //Get field definition
                var scienceType = types.First(x => x.Key == field.Key);

                //write value
                scienceEntryWriter.Write(scienceType.Type, field.Key, field.Value);
            }

            bsonWriter.WriteEndDocument();
        }

        public bool GetDocumentId(object document, out object id, out Type idNominalType, out IIdGenerator idGenerator)
        {
            var scienceEntry = document as ScienceEntry;

            if (scienceEntry == null) throw new ArgumentException("document is not of the ScienceEntry type and cannot be deserialised.", "document");

            id = scienceEntry.Id;
            idNominalType = typeof(ObjectId);
            idGenerator = ObjectIdGenerator.Instance;

            if (idGenerator.IsEmpty(id))
                id = ObjectId.GenerateNewId();
            return true;
        }

        public void SetDocumentId(object document, object id)
        {
            var scienceEntry = document as ScienceEntry;

            if (scienceEntry == null) throw new ArgumentException("document is not of the ScienceEntry type and cannot be serialised.", "document");

            scienceEntry.Id = (ObjectId)id;
        }
    }
}